Fork me on GitHub

Python HTTP 协议与 web 静态服务器

前言:

  1. 浏览器请求的基本流程;
  2. 浏览器请求的 URL;
  3. 请求报文格式;
  4. 响应报文格式;
  5. 网络响应状态码;
  6. 长连接和短连接。

HTTP 超文本传输协议

超文本传输协议(HyperText Transfer Protocol)是一种应用层协议。

HTTP是万维网的数据通信的基础。设计HTTP最初的目的是为了提供一种发布和接收HTML页面<网页>的方法。

  • 1989年蒂姆·伯纳斯-李在CERN研发
  • 1999年公布现今广泛使用的HTTP 1.1版(RFC2616)

一、浏览器请求的基本流程

mini-web服务器工作流程

二、浏览器请求的 URL

# www.baidu.com: 网站(网址)
# url(统一资源定位符):
# 完整版: http://www.baidu.com:80/aaa/bbb/index.html?username=aaa&password=123
# http/https: https是http加密后进行传输;(https收费...)
# 端口: http: 80; https: 443;
# /aaa/bbb/index.html: 请求的资源路径;
# username=aaa&password=123: 传输的内容;(请求体...GET)

三、请求报文格式总结


# 总结: 请求报文格式
# 1.请求行;
# GET / HTTP/1.1\r\n
# 2.请求头;
# 头属性: 属性值\r\n
# Host: www.baidu.com\r\n
# 3.空行;
# \r\n
# 4.请求体;
# username=jovelin&password=123456

# 请求报文格式分析:
# 1.请求行(request line)
# 格式: 请求方式 资源路径 协议及版本号\r\n
GET / HTTP/1.1\r\n
# GET: 常用请求方式GET/POST; (GET/POST/PUT/DELETE...)
# GET: 获取(从服务器获取信息的时候用...)
# POST: 发送(向服务器存储信息的时候用...)
# /: /aaa/bbb/index.html; 想要访问的页面/图片/音频...(明天要用...)
# HTTP/1.1: 协议及版本号
# 空行: \r\n

# 2.请求头(request header)
# 格式: 头属性: 头信息\r\n
Host: www.baidu.com\r\n
# Host: 主机;(记住...)
Connection: keep-alive\r\n
# Connection: 链接;(长连接)
Upgrade-Insecure-Requests: 1\r\n
# 提示服务端我可以解析https;
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36\r\n
# User-Agent: 用户代理;(浏览器及系统版本...)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
# Accept: 接收!
Accept-Encoding: gzip, deflate, br\r\n
# 压缩: 数据压缩算法;
Accept-Language: zh-CN,zh;q=0.9\r\n
# 语言: 中文;

# 3.空行;
# \r\n

# 4.请求体;
# username=jovelin&password=123456

请求报文格式总结

四、响应报文格式总结


# 总结: 响应报文格式
# 1.响应行;
# HTTP/1.1 200 OK\r\n
# 2.响应头;
# 头属性: 头信息\r\n
# Server: BWS/1.1\r\n
# 3.换行
# \r\n
# 4.响应体;
# 文本/图片/音频/视频/网页...

# 响应报文格式分析:
# 1.响应行(response line)
# 格式: 协议及版本号 状态码 英文解释\r\n
HTTP/1.1 200 OK\r\n
# HTTP/1.1: 协议及版本号
# 200 OK: 状态码 英文解释

# 2.响应头(response header)
# 格式: 头属性: 属性值\r\n
Connection: Keep-Alive\r\n
# 长连接
Content-Encoding: gzip\r\n
# 压缩格式
Content-Type: text/html; charset=utf-8\r\n
# 请求体的文本类型;
Date: Wed, 14 Mar 2018 09:52:48 GMT\r\n
# 更新时间
Server: BWS/1.1\r\n
# 服务器名:(记住,因为简单,以后用)

# 3.空行;
# \r\n

# 4.响应体(response body)
# 文本/图片/音频/视频/网页...

响应报文格式总结

五、网络响应状态码

2xx 成功  200 OK  (发送成功)
3xx 重定向
302 Moved Temporarily/302 Found 解释作用(暂时跳转) 301/2/3/4/7
307 Internal Redirect(内部重定向)
Location: https://www.baidu.com
4xx 客户端错误 404 Not Found(客户端发送的页面没找打)
http://help.xunlei.com/online/stat_inst.php?pid=0000&thunderver=5.8.14.706&thundertype=4&peerid=000C294E4AE1J3J4
http://video.baomihua.com/play_error/-30001
5xx 服务器错误 503 Service Unavailable(服务器不能使用)

六、长连接和短连接


TCP长/短连接 好比 地铁卡/单程票


在HTTP/1.0中, 默认使用的是短连接.也就是说, 浏览器和服务器每进行一次HTTP操作, 就建立一次连接, 但任务结束就中断连接.如果客户端浏览器访问的某个HTML或其他类型的 Web 页中包含有其他的Web资源,如js文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:

Connection:keep-alive

在真正的读写操作之前,server与client之间必须建立一个连接,

当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,

连接的建立通过三次握手,释放则需要四次握手,

所以说每个连接的建立都是需要资源消耗和时间消耗的。

TCP 短连接

短连接一般只会在 client/server 间传递一次读写操作!

  1. client 向 server 发起连接请求
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,此时双方任何一个都可以发起 close 操作 (一般都是 client 先发起 close 操作。当然也不排除有特殊的情况。)

TCP 长连接

  1. client 向 server 发起连接
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,连接不关闭
  6. 后续读写操作…
  7. 长时间操作之后 client 发起关闭请求

TCP长/短连接的优点和缺点

长连接可以省去较多的TCP建立和关闭的操作,节约时间。但是如果用户量太大容易造成服务器负载过高最终导致服务不可用。

短连接对于服务器来说实现起来较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但是如果用户访问量很大, 往往可能在很短时间内需要创建大量的连接,造成服务器响应速度过慢。

总之:

小的WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源来让套接字 保持存活-keep alive,

对于中大型WEB网站一般都采用长连接,好处是响应用户请求的时间更短,用户体验更好,虽然更耗硬件资源一些,但这都不是事儿。另外,数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误。

七、案例

1.模拟服务器(服务端)


# 需求: 获取请求报文的格式;

import socket

if __name__ == '__main__':
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
tcp_socket.bind(("127.0.0.1", 8888))
tcp_socket.listen(128)
print("服务已开启...")
while True:
service_client_socket, ip_port = tcp_socket.accept()
print(ip_port, "已连接...")
data_bin = service_client_socket.recv(5000)
print("二进制数据:", data_bin)
print("解析后数据:", data_bin.decode())
service_client_socket.close()

2.模拟浏览器(客户端)


# 需求: 获取响应报文的格式内容并保存;

import socket

if __name__ == '__main__':
# 创建TCP连接
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# DNS解析 和 连接HTTP服务器
tcp_socket.connect(("www.baidu.com", 80))

# 组包 发送HTTP请求报文

# 请求行
request_line = "GET / HTTP/1.1\r\n"

# 请求头
request_header = "Host: www.baidu.com\r\n"
request_data = request_line + request_header + "\r\n"

# 发送请求
tcp_socket.send(request_data.encode())

# 接收响应报文
response_data = tcp_socket.recv(4096)

# 对响应报文进行解析 -- 切割
response_str_data = response_data.decode()
# print(response_data)

# '\r\n\r\n'之后的数据就是响应体数据
index = response_str_data.find("\r\n\r\n")

# 切割出的数据就是文件数据
html_data = response_str_data[index + 4:]

# data_file = open("index.html", "wb")
# data_file.write(html_data.encode())
# data_file.close()
with open("index.html", "wb") as file:
file.write(html_data.encode())
# 如果是长连接,还有很多内容没有收到,需要死循环接收
while True:
# 后面在获取到的响应内容,就不包含响应行和响应头了
data_bin = tcp_socket.recv(4096)
if data_bin:
file.write(data_bin)
else:
break

# 关闭套接字
tcp_socket.close()
  1. web 静态服务器

from gevent import monkey

monkey.patch_all()

import socket
import gevent
import sys


class WebServer(object):
"""Web 服务器类"""

def __init__(self, ip, port):
# 创建套接字
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置套接字复用地址
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定IP地址和端口
self.socket.bind((ip, port))
# 设置被动套接字
self.socket.listen(128)

def startup(self):
"""等待客户端连接"""
while True:
# 等待被连接
service_client_socket, ip_port = self.socket.accept()
print(ip_port, "连接成功.", end="\n\n")
# 处理请求
gevent.spawn(self.client_handler, service_client_socket)

def client_handler(self, service_client_socket):
"""处理客户端请求"""
request_data_bin = service_client_socket.recv(4096)

if not request_data_bin:
print('客户端已经断开连接.', end="\n\n")
service_client_socket.close()
return
else:
print("客户端请求报文:", request_data_bin, end="\n\n")

# 解析 HTTP 文本
my_http = self.parse_http(request_data_bin.decode('utf-8'))

# 读取固定页面数据
try:
response_line = 'HTTP/1.1 200 OK\r\n'
response_header = 'Server: PythonWebServer1.0\r\n'
file = open('./static' + my_http['url'], 'rb')
except:
response_line = 'HTTP/1.1 404 NOT FOUND\r\n'
response_header = 'Server: PythonWebServer1.0\r\n'
file = open('./static/404.html', 'rb')

# 读取文件内容
response_content = file.read()
file.close()
# 拼接响应报文
response_data = (response_line + response_header + '\r\n').encode('utf-8') + response_content
# 发送响应报文
service_client_socket.send(response_data)
# 关闭套接字
service_client_socket.close()

print("\n", "-" * 100, "\n")

@staticmethod
def parse_http(request_data):
"""解析 HTTP 文本"""

my_http = {}

# 分成多行
request_headers = request_data.split('\r\n')
request_lines = request_headers[0].split(' ')
print("request_lines: ", request_lines, end="\n\n")

my_http['method'] = request_lines[0]
my_http['url'] = request_lines[1]
my_http['version'] = request_lines[2]

# 未指定页面时 默认访问 index.html
if my_http['url'] == "/":
my_http['url'] = "/index.html"

for header in request_headers[1:]:

if not header:
continue

# Host: www.baidu.com
lines = header.split(':')
my_http[lines[0]] = lines[1][1:]

print("my_http: ", my_http, end="\n\n")

return my_http


def port_handler():
"""指定端口"""

# 默认设置端口为 8888
port = 8888

# # 获取外部传递过来的参数;
# # 1.尽量值传递一个参数过来
# # ctrl+z: 退出页面,但是程序没有退出;(该端口还可以使用)
# # ctrl+c: 退出页面,也退出程序;
# # print(sys.argv[1])
# if not len(sys.argv) == 2:
# print('输入的格式错误,正确的格式应该是: python3 文件名.py 端口号')
# return
#
# # 2.如果传递过来端口号,里面有非数字;(也不行)
# if not sys.argv[1].isdigit():
# print('端口号, 必须是整数!!!')
# return
#
# # 3.取值范围: [0-65535]
# if not 0 <= int(sys.argv[1]) <= 65535:
# print('端口号必须在: [0-65535]之间!!!')
# return
#
# # 4.如果全部通过,那么要把端口号,传递到程序中
# # 获取用户指定的绑定端口
# port = int(sys.argv[1])

return port


def main():
# 服务器 IP,默认为本机 IP
server_ip = ""
# 服务器 端口
server_port = port_handler()
web_server = WebServer(server_ip, server_port)
web_server.startup()


if __name__ == '__main__':
main()
-------------本文结束感谢您的阅读-------------

本文标题:Python HTTP 协议与 web 静态服务器

文章作者:曹永林

发布时间:2018年07月15日 - 20:07

最后更新:2018年07月28日 - 10:07

原始链接:http://jovelin.cn/2018/07/15/Python HTTP 协议与 web 静态服务器/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。